package cevent.source.cloudcenter.server.config;/**
 * Created by Cevent on 2021/3/20.
 */

import cevent.source.cloudcenter.server.util.UUID8Util;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;

/**
 * @author cevent
 * @description aspect orient programming面向切面的统一日志管理配置
 * component：声明spring需要扫描的组件
 * @date 2021/3/20 11:31
 */
@Aspect
@Component
public class AOPLog {
    public static final Logger LOG= LoggerFactory.getLogger(AOPLog.class);
    //1.定义切面:所有包下*Controller的方法
    @Pointcut("execution(public * cevent.source.cloudcenter.*.controller..*Controller.*(..))")
    public void controllerPointCutAOP(){

    }
    //2.切点开始之前执行
    @Before("controllerPointCutAOP()")
    public void doBefore(JoinPoint joinPoint) throws Throwable{
        //2.1"UUID"->日志编号：日志流水token
        // 一个日志token用来跟踪一个请求(每个请求的UUID日志流水号唯一)
        // 生产环境往往同时打印多个请求，通过grep 日志token来查找每一次请求
        //key="UUID"与logback中的encoder-pattern变量名相同
        // MDC日志框架slf4j内置变量配置
        /**
         * <Pattern>
         *     %d{ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) $green(%-8X{UUID}) %msg%n
         * </Pattern>
         */
        MDC.put("UUID", UUID8Util.getShortUUID());
        //2.2 servlet请求(请求头)--> 打印请求日志
        ServletRequestAttributes servletRequestAttributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //获取http：所有的请求都可以从这里的servlet获取
        HttpServletRequest httpServletRequest=servletRequestAttributes.getRequest();
        //2.3获取签名信息
        Signature signature=joinPoint.getSignature();
        //获取签名后，获取void方法头
        String signatureName=signature.getName();
        //2.4打印业务操作
        String serviceOperation="";
        //contains有约定要求，方法首位必须是list or save ...
        if(signatureName.contains("list") || signatureName.contains("query") || signatureName.contains("get") || signatureName.contains("find")){
            serviceOperation="查询";
        }
        else if (signatureName.contains("save") || signatureName.contains("add") || signatureName.contains("insert") || signatureName.contains("edit") || signatureName.contains("update")){
            serviceOperation="保存";
        }
        else if(signatureName.contains("delete") || signatureName.contains("del") || signatureName.contains("remove")){
            serviceOperation="删除";
        }else if(signatureName.contains("upload")){
            serviceOperation="文件上传";
        }else{
            serviceOperation="更新其他操作";
        }

        //3.反射：获取业务名称（签名声明类型:BUSINESS_NAME）
        Class clazz=signature.getDeclaringType();
        Field field;
        String serverName="";
        //获取到的签名域不为空
        try {
            //获取Controller下的public string BUSINESS_NMAE="xxx"
            field=clazz.getField("BUSINESS_NAME");
            if(!StringUtils.isEmpty(field)){
                serverName=(String)field.get(clazz);
            }
        }  catch (SecurityException e) {
            LOG.info("没有获取到服务名称，默认 [ BUSINESS_NAME ] : ",e);
        }

        //4.打印请求
        LOG.info("---BUSINESS【{}】 {} 开始 ---",serverName,serviceOperation);
        LOG.info("请求地址：{} {}",httpServletRequest.getRequestURL().toString(),httpServletRequest.getMethod());
        LOG.info("类名：{} 方法：{}",signature.getDeclaringTypeName(),signatureName);
        LOG.info("远程地址：{}",httpServletRequest.getRemoteAddr());

        //5.打印请求参数
        Object[] args=joinPoint.getArgs();
        Object[] arguments=new Object[args.length];
        for(int i=0;i<args.length;i++){
            //去除的请求参数类型request、response、multipartFile文件类型
            if(args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile){
                continue;
            }
            arguments[i]=args[i];
        }

        //6.排除字段，敏感字段(身份证/手机号等)或太长的字段(图片会转为base64长文本)不显示，定义碎片拦截shard
        String[] excludeProperties={"shard"};
        //引入AspectJ 阿里图片转换fastjson，进行图片拦截，这里需要多等一会儿alt+enter
        PropertyPreFilters preFilters=new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludeFilter=preFilters.addFilter();
        excludeFilter.addExcludes(excludeProperties);
        //为空不打印，但是类图片等长字段会打印（引用存入的arguments请求类型）
        LOG.info("exclude filter：请求参数：{}", JSONObject.toJSONString(arguments,excludeFilter));
    }

    //环绕通知：打印返回信息，进程切点
    @Around("controllerPointCutAOP()")
    public Object returnAroundMessage(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        //打印时间：结束-开始执行时间
        long startTime=System.currentTimeMillis();
        //执行业务proceed代码进程记录，结束后返回return
        Object result=proceedingJoinPoint.proceed();
        //排除字段，敏感字段或太长的字段不显示，前端进行datForam参数转换param时，将file转为base64，进行shard:传输，太长需去掉打印
        String[] excludeProperties={"password","shard"};
        PropertyPreFilters preFilters=new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludeFilter=preFilters.addFilter();
        excludeFilter.addExcludes(excludeProperties);
        LOG.info("@Around返回结果：{}",JSONObject.toJSONString(result,excludeFilter));
        LOG.info("----------------结束耗时：{} ms------------------",System.currentTimeMillis()-startTime);

        return result;
    }
}
